﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Diagnostics.Contracts;
using System.Reflection;

using E = System.Linq.Expressions.Expression;
using OE = ReactiveCode.Observables.Expressions.ObservableExpressionExtensions;

namespace ReactiveCode.Observables.Expressions
{
    using Util;
    using Util.Expressions;

    class ObservableConverter : ExpressionVisitor<Expression>
    {
        public Expression Convert(Expression expression)
        {
            Contract.Requires(expression != null);
            return Visit(expression);
        }
        public IObservable<T> ConvertEval<T>(Expression expression)
        {
            return ExpressionUtil.Compile<IObservable<T>>(Convert(expression))();
        }

        internal IEnumerable<E> Visit(IEnumerable<E> expressions)
        {
            if (expressions == null)
                return Enumerable.Empty<E>();
            return expressions.Select(Visit);
        }

        protected override Expression Visit(Expression expr)
        {
            if (expr == null)
                return null;

            switch (expr.NodeType)
            {
                case ExpressionType.AddAssign:
                case ExpressionType.AddAssignChecked:
                case ExpressionType.AndAssign:
                case ExpressionType.Assign:
                case ExpressionType.Block:
                case ExpressionType.DebugInfo:
                case ExpressionType.DivideAssign:
                case ExpressionType.Dynamic: // todo
                case ExpressionType.ExclusiveOrAssign:
                case ExpressionType.Extension:
                case ExpressionType.Goto:
                case ExpressionType.Increment:
                case ExpressionType.Label:
                case ExpressionType.LeftShiftAssign:
                case ExpressionType.Loop:
                case ExpressionType.ModuloAssign:
                case ExpressionType.MultiplyAssign:
                case ExpressionType.MultiplyAssignChecked:
                case ExpressionType.OnesComplement:
                case ExpressionType.OrAssign:
                case ExpressionType.PostDecrementAssign:
                case ExpressionType.PostIncrementAssign:
                case ExpressionType.PowerAssign:
                case ExpressionType.PreDecrementAssign:
                case ExpressionType.PreIncrementAssign:
                case ExpressionType.RightShiftAssign:
                case ExpressionType.RuntimeVariables:
                case ExpressionType.SubtractAssign:
                case ExpressionType.SubtractAssignChecked:
                    throw new NotSupportedException(string.Format("Expression type {0} is not supported: {1}", expr.NodeType, expr));
            }

            return base.Visit(expr);
        }

        #region Type members
        protected virtual Expression ListenMemberCore(Expression observableInstance, MemberInfo member, Expression observableArguments)
        {
            var listener = ChangeListening.CreateListener(member, false);
            if (listener == null)
                return null;
            var listenerExpr = Expression.Constant(listener);

            observableInstance = observableInstance ?? Null;
            Type instanceType = observableInstance.GetElementType();

            if (observableArguments != null)
            {
                Type argType = observableArguments.GetElementType();
                var p = E.Parameter(typeof(Tuple<,>).MakeGenericType(instanceType, argType), "p");

                var arg = E.Property(p, "Item2");
                var method = member as MethodInfo ?? ((member as PropertyInfo).GetGetMethod() ?? (member as PropertyInfo).GetSetMethod());
                if (method == null)
                    return null;
                int argCount = method.GetParameters().Length;
                if (argCount > 1)
                    Contract.Assert(argType.Name.StartsWith("Tuple"));
                IEnumerable<E> args = argCount == 1 
                    ? Enumerable.Repeat(arg, 1)
                    : Enumerable.Range(1, argCount).Select(i => E.Property(arg, "Item" + i));
                args = args.Select(a => E.Convert(a, typeof(object)) as E);

                var invokeListener = E.Lambda(
                    E.Invoke(listenerExpr, E.Property(p, "Item1"), E.NewArrayInit(typeof(object), args)),
                    p);

                return observableInstance.CombineLatest(observableArguments).SelectMany(invokeListener);
            }
            else if (observableInstance == null)
            {
                return E.Invoke(listenerExpr, E.Convert(null, typeof(object)), E.Constant(null, typeof(object[])));
            }
            else
            {
                var item = E.Parameter(instanceType, "x");
                var invokeListener = E.Invoke(listenerExpr, E.Convert(item, typeof(object)), E.Constant(null, typeof(object[])));
                return observableInstance.SelectMany(E.Lambda(invokeListener, item));
            }
        }

        static Expression Null = Expression.Constant(Observable.Return<object>(null));
        Expression ListenMember(Expression instance, MemberInfo member, IList<Expression> arguments, Func<Expression, Expression[], Expression> getValue)
        {
            member = member.Normalize();
            instance = instance != null ? Visit(instance) : null;
            var observableArgs = (arguments != null && arguments.Any()) ? Visit(arguments).CombineLatest() : null;

            var values = ListenMemberCore(instance, member, observableArgs);
            if (values != null)
                return values.Cast(ReflectionUtil.GetValueType(member)).DistinctUntilChanged();

            if (instance == null && observableArgs == null)
                return OE.Return(getValue(null, null));

            if (instance == null)
                return Visit(arguments).CombineLatest(args => getValue(null, args));

            var item = E.Parameter(instance.GetElementType(), "x");
            if (observableArgs == null)
            {
                return instance.Select(E.Lambda(getValue(item, null), item));
            }
            else
            {
                var compositeArg = E.Parameter(observableArgs.GetElementType(), "args");
                var args = (arguments.Count == 1) 
                    ? new[] { compositeArg as Expression }
                    : arguments.Select((_, i) => E.Property(compositeArg, "Item" + (i + 1))).ToArray();
                return instance.CombineLatest(observableArgs, E.Lambda(getValue(item, args), item, compositeArg));
            }
        }

        protected override Expression VisitMember(MemberExpression exp)
        {
            return ListenMember(exp.Expression, exp.Member, null, (x, _) => E.MakeMemberAccess(x, exp.Member));
        }
        protected override Expression VisitIndex(IndexExpression exp)
        {
            return ListenMember(exp.Object, exp.Indexer, exp.Arguments, (x, args) => E.MakeIndex(x, exp.Indexer, args));
        }
        protected override Expression VisitMethodCall(MethodCallExpression exp)
        {
            if (exp.Method.ReturnType != typeof(void))
            {
                return ListenMember(exp.Object, exp.Method, exp.Arguments, (x, args) => E.Call(x, exp.Method, args));
            }

            return base.VisitMethodCall(exp);
        }

        #endregion

        protected override Expression VisitInvocation(InvocationExpression exp)
        {
            var allSources = new List<E> { Visit(exp.Expression) };
            allSources.AddRange(Visit(exp.Arguments));

            return allSources.CombineLatest(x => E.Invoke(x[0], x.Skip(1)));
        }
        protected override Expression VisitConstant(ConstantExpression exp)
        {
            return OE.Return(exp);
        }
        protected override Expression VisitBinary(BinaryExpression exp)
        {
            var left = Visit(exp.Left);
            var right = Visit(exp.Right);

            var x = E.Parameter(exp.Left.Type);
            var y = E.Parameter(exp.Right.Type);
            var selector = E.Lambda(E.MakeBinary(exp.NodeType, x, y), x, y);

            return left.CombineLatest(right, selector);
        }
        protected override Expression VisitConditional(ConditionalExpression exp)
        {
            return Visit(exp.Test).IIf(Visit(exp.IfTrue), Visit(exp.IfFalse));
        }
        protected override Expression VisitUnary(UnaryExpression exp)
        {
            var operand = Visit(exp.Operand);

            var x = E.Parameter(exp.Operand.Type);
            var selector = Expression.Lambda(Expression.MakeUnary(exp.NodeType, x, exp.Type), x);

            return operand.Select(selector);
        }
        protected override Expression VisitDefault(DefaultExpression exp)
        {
            return OE.Return(exp);
        }

        protected override Expression VisitLambda(LambdaExpression exp)
        {
            var newBody = Expression.Parameter(exp.Body.Type);
            var createLambda = E.Lambda(
                E.Lambda(newBody, exp.Parameters),
                newBody);

            return Visit(exp.Body).Select(createLambda);
        }

        protected override Expression VisitNew(NewExpression nex)
        {
            return Visit(nex.Arguments).ToList()
                .CombineLatest(args => Expression.New(nex.Constructor, args));
        }

        #region Member init
        //IObservable<Action<object>> Visit(ElementInit init)
        //{
        //    return Visit(init.Arguments).Select<object[], Action<object>>(args => obj => init.AddMethod.Invoke(obj, args));
        //}
        //IObservable<Action<object>> Visit(MemberBinding binding)
        //{
        //    switch (binding.BindingType)
        //    {
        //        case MemberBindingType.Assignment:
        //            {
        //                var assign = (MemberAssignment)binding;
        //                var setter = ReflectionUtil.ToSetter(binding.Member);
        //                Contract.Assert(setter != null);
        //                return Visit(assign.Expression).Select<object, Action<object>>(x => obj => setter(obj, null, x));
        //            }
        //        case MemberBindingType.ListBinding:
        //            {
        //                var list = (MemberListBinding)binding;
        //                return list.Initializers.Select(Visit).CombineLatest()
        //                    .Select<Action<object>[], Action<object>>(addElements => obj => addElements.ForEach(a => a(obj)));
        //            }
        //        case MemberBindingType.MemberBinding:
        //            {
        //                var member = (MemberMemberBinding)binding;
        //                var getter = ReflectionUtil.ToGetter(binding.Member);
        //                return Visit(member.Bindings).Select<Action<object>[], Action<object>>(actions => obj =>
        //                {
        //                    object memberValue = getter(obj, null);
        //                    foreach (var a in actions)
        //                        a(memberValue);
        //                });
        //            }
        //        default:
        //            throw new ArgumentException("binding");
        //    }
        //}
        //IObservable<Action<object>[]> Visit(IEnumerable<MemberBinding> binding)
        //{
        //    return binding.Select(Visit).CombineLatest();
        //}
        protected override Expression VisitMemberInit(MemberInitExpression exp)
        {
            throw new NotImplementedException();
            //return VisitNew(exp.NewExpression).CombineLatest(Visit(exp.Bindings), (obj, bindings) =>
            //{
            //    foreach (var b in bindings)
            //        b(obj);
            //    return obj;
            //});
        }
        #endregion

        protected override Expression VisitNewArray(NewArrayExpression exp)
        {
            var expressions = Visit(exp.Expressions).ToList();
            Type elementType = exp.Type.GetElementType();

            switch (exp.NodeType)
            {
                case ExpressionType.NewArrayBounds:
                    return expressions.CombineLatest(e => E.NewArrayBounds(elementType, e));

                case ExpressionType.NewArrayInit:
                    return expressions.CombineLatest(e => E.NewArrayInit(elementType, e));

                default:
                    throw new ArgumentException("exp");
            }
        }
        protected override Expression VisitTypeBinary(TypeBinaryExpression exp)
        {
            var x = E.Parameter(exp.Expression.Type);
            Expression selectorBody;
            if (exp.NodeType == ExpressionType.TypeIs)
                selectorBody = E.TypeIs(x, exp.TypeOperand);
            else if (exp.NodeType == ExpressionType.TypeAs)
                selectorBody = E.TypeAs(x, exp.TypeOperand);
            else
                throw new ArgumentException("exp");

            return Visit(exp.Expression).Select(E.Lambda(selectorBody, x));
        }
    }
}
